package encription;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.security.*;
import java.util.Arrays;

import javax.crypto.*;

import packets.Packet;
import sun.misc.BASE64Decoder;
import sun.misc.BASE64Encoder;

import client.logic.GuiFacade;
import exceptions.InvalidPacketContentException;
import exceptions.RegistrationExeption;
import exceptions.UnverifiedMessageException;

/**
 * Service class that handles encryptioning/decryptioning , serializing/deserializing ,signing/verifying messages and reading/writing objects to files
 */
public class SecurityHandler {
	
	/**
	 * receives an array of bytes and decrypts it using RSA with the given private key
	 * @param data the data to be decrypted
	 * @param privatekey the RSA private key to decrypt with
	 * @param size length of the decrypted content inside the buffer
	 * @return the decrypted data
	 * @throws NoSuchAlgorithmException
	 * @throws NoSuchPaddingException
	 * @throws InvalidKeyException if the key is wrong
	 * @throws ShortBufferException
	 * @throws IllegalBlockSizeException
	 * @throws BadPaddingException
	 */
	public static byte[] decrypt(byte[] data,PrivateKey privatekey, int size) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, ShortBufferException, IllegalBlockSizeException, BadPaddingException{
		byte[] decryptedText = null;
		Cipher cipher;
		cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
		cipher.init(Cipher.DECRYPT_MODE, privatekey);
		decryptedText = new byte[data.length];
		//decrypts the message in parts
		int inOffset = 0;
		int outOffset = 0;
		int bytesWritten = 100;
		while(inOffset < size){
			bytesWritten = cipher.doFinal(data,inOffset,Math.min(128, size-inOffset) ,decryptedText,outOffset);
			inOffset+=128;
			outOffset+=bytesWritten;
		}
		return decryptedText;

	}
	/**
	 * produces a new RSA key
	 * @return a new RSA key
	 * @throws NoSuchAlgorithmException
	 */
	public static KeyPair makeKey() throws NoSuchAlgorithmException{
		KeyPairGenerator keyGen = null;
		keyGen = KeyPairGenerator.getInstance("RSA");
		keyGen.initialize(1024);		//generates 1024 bits RSA key
		KeyPair key = keyGen.generateKeyPair();
		return key;
	}
	
	/**
	 * encrypts a given message using the given public key
	 * @param msg the message to encrypt
	 * @param publickey the key to encrypt the message with
	 * @return the encrypted message or the original message if the key is null
	 * @throws NoSuchAlgorithmException
	 * @throws NoSuchPaddingException
	 * @throws InvalidKeyException
	 * @throws ShortBufferException
	 * @throws IllegalBlockSizeException
	 * @throws BadPaddingException
	 */
	public static byte[] encrypt(byte[] msg,PublicKey publickey) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, ShortBufferException, IllegalBlockSizeException, BadPaddingException{
		if (publickey == null)
			return msg;
		Cipher cipher;
		cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
		cipher.init(Cipher.ENCRYPT_MODE, publickey);
		//encrypt the message in parts
		int outputLength = (msg.length / 100)*cipher.getOutputSize(100) + ((msg.length % 100)==0 ? 0 : cipher.getOutputSize(0));
		byte[] cipherText = new byte[outputLength];
		int inOffset = 0;
		int outOffset = 0;
		while(inOffset < msg.length){
			int bytesWritten = cipher.doFinal(msg,inOffset,Math.min(100, msg.length-inOffset) ,cipherText,outOffset);
			inOffset+=100;
			outOffset+=bytesWritten;
		}
		return cipherText;
	}

	/**
	 * verifies a message with a given signature and a public key
	 * @param plainText the text to verify
	 * @param signature the signature for the text
	 * @param publicKey the key to verify the signature with
	 * @return true if the message is verified and indeed fits the given key
	 */
	public static boolean verify(byte[] plainText,byte[] signature,PublicKey publicKey){
		try {
			Signature sig = Signature.getInstance("MD5WithRSA");
			sig.initVerify(publicKey);
			sig.update(plainText);
			return sig.verify(signature);	//verify using RSA
		} catch (SignatureException se) {
			//e.printStackTrace();
			return false;
		} catch (InvalidKeyException e) {
			//e.printStackTrace();
			return false;
		} catch (NoSuchAlgorithmException e) {
			//e.printStackTrace();
			return false;
		}
	}
	
	/**
	 * produces a signature for a given message , using the given private key
	 * @param plainText the message to sign
	 * @param privateKey the key to use for signing
	 * @return the signature for the given message
	 * @throws NoSuchAlgorithmException
	 * @throws InvalidKeyException
	 * @throws SignatureException
	 * @throws UnsupportedEncodingException
	 */
	public static byte[] sign(byte[] plainText,PrivateKey privateKey) throws NoSuchAlgorithmException, InvalidKeyException, SignatureException, UnsupportedEncodingException{
		Signature sig = Signature.getInstance("MD5WithRSA");
		sig.initSign(privateKey);
		sig.update(plainText);
		byte[] signature = sig.sign();	//sign using RSA
		return signature;
	}

	/**
	 * receives an object and returns a serialization of it
	 * @param o the object to serialize
	 * @return the serialization of the given object 
	 * @throws IOException
	 */
	public static byte[] getBytes(Object o) throws IOException{
		//serialize the object using streams
		ByteArrayOutputStream bos = new ByteArrayOutputStream();
		ObjectOutputStream oos = new ObjectOutputStream(bos);;
		oos.writeObject(o);
		oos.flush();
		oos.close();
		return bos.toByteArray();
	}

	/**
	 * deserializes an array of bytes into an Object
	 * @param b the array to deserialize
	 * @return the resulted object from the deserializxation process
	 * @throws IOException
	 * @throws ClassNotFoundException if the content of the give array is not an {@link Object}
	 */
	public static Object getObject(byte[] b) throws IOException, ClassNotFoundException{
		ByteArrayInputStream bos = new ByteArrayInputStream(b);
		ObjectInputStream oos;
		oos = new ObjectInputStream(bos);
		Object res = oos.readObject();
		oos.close();
		return res;
	}

	/**
	 * receives a signed and encrypted message from a source , the message is verified, decrypted and turned back into a Packet object
	 * @param signatureKey the key to verify the signature with
	 * @param decryptionKey the key to decrypt the message with
	 * @param source the source to get the message from
	 * @return the Packet that the server send
	 * @throws Exception
	 */
	public static Packet getMessage(PublicKey signatureKey, PrivateKey decryptionKey , InputStream source) throws Exception{
		byte[] content = new byte[8192];
		int msgSize =  source.read(content);
		byte[] message = Arrays.copyOfRange(content, 0, (msgSize-128));
		byte[] signature = Arrays.copyOfRange(content, msgSize-128, msgSize);

		if (!SecurityHandler.verify(message, signature, signatureKey)){
			throw new UnverifiedMessageException("Server did not sign this message");
		} else {
			return (Packet)(SecurityHandler.getObject(SecurityHandler.decrypt(message, decryptionKey, message.length)));
		}
	}

	/**
	 * sends a Packet to the destination , encrypted and signed with the given keys.
	 * @param packet the packet to send
	 * @param encryptionKey the key to encrypt the message with
	 * @param signatureKey the key to sign the message with
	 * @param out the destination to send the message to
	 * @throws Exception
	 */
	public static void sendEncryptedMessage(Packet packet, PublicKey encryptionKey , PrivateKey signatureKey , OutputStream out) throws Exception{
		byte[] encrypted = SecurityHandler.encrypt(SecurityHandler.getBytes(packet), encryptionKey);
		byte[] signature = SecurityHandler.sign(encrypted, signatureKey);
		out.write(encrypted);
		//System.out.println("sent data "+ encrypted.length +" bytes");
		out.write(signature);
		//System.out.println("sent signature " + signature.length + " bytes");
		out.flush();
	}

	/**
	 * encrypts a given char array using SHA-1 (used to encrypt passwords)
	 * @param x the array to encrypt
	 * @return the encrypted data
	 * @throws NoSuchAlgorithmException
	 */
	public static String encryptPassword(char[] x) throws NoSuchAlgorithmException  // throws Exception
	{
		java.security.MessageDigest d =null;
		d = java.security.MessageDigest.getInstance("SHA-1");
		d.reset();
		byte[] arr = new byte[x.length];
		for (int i=0 ; i<x.length ; i++){
			arr[i] = (byte) x[i];
		}
		d.update(arr);
		String s = new String(d.digest());
		return s;
	}

	/**
	 * reads an Object from a the file (decodes the file using base64)
	 * @param filename file name to read from
	 * @return the Object contained in the file
	 * @throws IOException in case there is an error reading from the file
	 * @throws ClassNotFoundException in case the file content wasn't recognized
	 */
	public static Object readFromFile(String filename) throws IOException, ClassNotFoundException{
		BASE64Decoder d = new BASE64Decoder();
		FileInputStream fis = new FileInputStream(filename);
		byte[] bytes = d.decodeBuffer(fis); //decodes the content of the file 
		return getObject(bytes);
	}

	/**
	 * writes an Object to a file , encoded using base64
	 * @param name the file name to write to
	 * @param o the Object to write to the file
	 * @throws IOException in case there was an error writing to the file
	 */
	public static void writeToFile(Object o, String name) throws IOException{
		BASE64Encoder e = new BASE64Encoder();
		byte[] bytes = getBytes(o);
		FileOutputStream fis = new FileOutputStream(name);
		e.encodeBuffer(bytes, fis);	//encodes the content into the files
	}
	

	/**
	 * prints an error message for a given {@link Exception}
	 * @param operation the operation in which the exception was thrown in
	 * @param exception the {@link Exception}
	 * @param isGui indicates if the error message should be printed to a client GUI , or to the prompt
	 */
	public static void catchException(String operation, Exception exception, boolean isGui){
		String msg;
		try {
			throw exception;
		} catch (SocketException e){
			msg = ("Connection error " + operation);
		} catch (FileNotFoundException e){
			msg = ("Could not find file for " + operation);
			//e.printStackTrace();
		} catch (ClassNotFoundException e) {
			msg = ("Wrong file content when reading from file while " + operation);
			//e.printStackTrace();
		} catch (NoSuchAlgorithmException e) {
			msg = ("Unable to find encryption algorithm when " + operation);
			//e.printStackTrace();
		} catch (RegistrationExeption e) {
			msg = ("Cant register with server ");
			//e.printStackTrace();
		} catch (UnknownHostException e) {
			msg = ("Unable to find server when " + operation);
			//e.printStackTrace();
		} catch (IOException e) {
			msg = ("Recieved IOException when " + operation);
			//e.printStackTrace();
		} catch (UnverifiedMessageException e) {
			msg = ("Error when verifiying server's reply wehn " + operation);
			//e.printStackTrace();
		} catch (InvalidPacketContentException e) {
			msg = ("Wrong reply from server when " + operation);
			//e.printStackTrace();
		} catch (InvalidKeyException e) {
			msg = ("Error with encryption/decryption when " + operation + " - " + e.getLocalizedMessage());
			//e.printStackTrace();
		} catch (NoSuchPaddingException e) {
			msg = ("Error with encryption/decryption when " + operation + " - " + e.getLocalizedMessage());
			//e.printStackTrace();
		} catch (ShortBufferException e) {
			msg = ("Error with encryption/decryption when " + operation + " - " + e.getLocalizedMessage());
			//e.printStackTrace();
		} catch (IllegalBlockSizeException e) {
			msg = ("Error with encryption/decryption when " + operation + " - " + e.getLocalizedMessage());
			//e.printStackTrace();
		} catch (BadPaddingException e) {
			msg = ("Error with encryption/decryption when " + operation + " - " + e.getLocalizedMessage());
			//e.printStackTrace();
		} catch (SignatureException e) {
			msg = ("Error with encryption/decryption when " + operation + " - " + e.getLocalizedMessage());
			//e.printStackTrace();
		} catch (Exception e){
			msg = ("Unknown Exception");
			//e.printStackTrace();
		}
		if (isGui){
			GuiFacade.getInstance().showErrorMessage(msg);
		} else {
			System.out.println(msg);
		}
	}
}